上篇 JS 的博客写了我看大牛划分的 JS 顶层体系分类,还记得吗?是 语义、文法、运行时,这篇文章就从运行时的角度去看 JavaScript 的类型系统。

运行时类型是代码实际执行过程中我们用到的类型。所有的类型数据都会属于7个类型之一。从变量、参数、返回值到表达式中间结果,任何JavaScript代码运行过程中产生的数据,都具有运行时类型。

在正式开始之前,我先留下几个问题,如果能全部理解这些问题,那就很容易就能理解后面的内容,不过有疑惑,那就带着问题去下面的内容中找答案吧,请听题:

  • 为什么有的编程规范要求用void 0代替undefined?
  • 字符串有最大长度吗?
  • 0.1 + 0.2不是等于0.3么?为什么JavaScript里不是这样的?
  • ES6新加入的Symbol是个什么东西?
  • 为什么给对象添加的方法能用在基本类型上?

类型

avaScript语言的每一个值都属于某一种数据类型。JavaScript语言规定了7种语言类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。根据最新的语言标准,这7种语言类型是:

  1. Undefined;
  2. Null;
  3. Boolean;
  4. String;
  5. Number;
  6. Symbol;
  7. Object。

除了ES6中新加入的Symbol类型,剩下6种类型都是常见的数据类型了,但是,要想回答文章一开始的问题,就要重新复习下这些数据类型了。

Undefined、Null

第一个问题是,为什么有的编程规范要求用void 0代替undefined?现在我们就分别来看一下。

Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined,一般我们可以用全局变量undefined(就是名为undefined的这个变量)来表达这个值,或者 void 运算来把任一一个表达式变成 undefined 值。

但是JavaScript的代码undefined是一个变量,而并非是一个关键字,因此为了避免undefined在局部环境中无意中被篡改,因此使用 void 0 来获取undefined值。在全局环境中 undefined是无法被篡改的。

Undefined跟 null 有一定的表意差别,null表示的是:“定义了但是为空”。所以,在实际编程时,我们一般不会把变量赋值为 undefined,这样可以保证所有值为 undefined 的变量,都是从未赋值的自然状态。

Null 类型也只有一个值,就是 null,它的语义表示空值,与 undefined 不同,null 是 JavaScript 关键字,所以在任何代码中,都可以放心用 null 关键字来获取 null 值。

Boolean

Boolean 类型有两个值, true 和 false,它用于表示逻辑意义上的真和假,同样有关键字 true 和 false 来表示两个值。这个类型很简单,就不做过多介绍了。

String

String 用于表示文本数据。String 有最大长度是 2^53 - 1,不过因为String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。

JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征

Number

Number类型表示我们通常意义上的“数字”。

JavaScript中的Number类型有 18437736874454810627(即2^64-2^53+3) 个值。但是JavaScript为了表达几个额外的语言场景(比如不让除以0出错,而引入了无穷大的概念),规定了几个例外情况:

  • NaN,占用了 9007199254740990,这原本是符合IEEE规则的数字,在JS表示为:不是数字;
  • Infinity,无穷大;
  • -Infinity,负无穷大。

另外,值得注意的是,JavaScript中有 +0 和 -0,在加法类运算中它们没有区别,但是除法的场合则需要特别留意区分,“忘记检测除以-0,而得到负无穷大”的情况经常会导致错误,而区分 +0 和 -0 的方式,正是检测 1/x 是 Infinity 还是 -Infinity。

根据双精度浮点数的定义,Number类型中有效的整数范围是-0x1fffffffffffff至0x1fffffffffffff,所以Number无法精确表示此范围外的整数。同样根据浮点数的定义,非整数的Number类型无法用 ==(===也不行) 来比较,一段著名的代码,这也正是第三题的问题,为什么在JavaScript中,0.1+0.2不能=0.3:

1
console.log( 0.1 + 0.2 == 0.3);

这里输出的结果是false,说明两边不相等的,这是浮点运算的特点,浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用JavaScript提供的最小精度值:

1
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。

Symbol

Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象key的集合,在ES6规范中,整个对象系统被用Symbol 重塑。

Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol也不相等。创建 Symbol 的方式是使用全局的 Symbol 函数。例如:

1
var mySymbol = Symbol("my symbol");

一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。例如,可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
var o = new Object

o[Symbol.iterator] = function() {
var v = 0
return {
next: function() {
return { value: v++, done: v > 10 }
}
}
};

for(var v of o)
console.log(v); // 0 1 2 3 ... 9

代码中定义了iterator之后,用for(var v of o)就可以调用这个函数,然后我们可以根据函数的行为,产生一个for…of的行为。

这里给对象o添加了 Symbol.iterator 属性,并且按照迭代器的要求定义了一个0到10的迭代器,之后我们就可以在for of中愉快地使用这个o对象啦。

这些标准中被称为“众所周知”的 Symbol,也构成了语言的一类接口形式。它们允许编写与语言结合更紧密的 API。

Symbol内容很多,这里只是最基本的内容,以后再详细写写。

Object

Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。Object表示对象的意思,它是一切有形和无形物体的总称。

最后我们来看一看,为什么给对象添加的方法能用在基本类型上?

在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是key-value结构,key可以是字符串或者 Symbol类型。

JS中的对象和C#中的对象区别:

提到对象,必须要提到一个概念:类。在C#和Java中,每个类都是一个类型,二者几乎等同,以至于很多人常常会把JavaScript的“类”与类型混淆。事实上,JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而JavaScript中是无法自定义类型的。

JavaScript中的几个基本类型,都在对象类型中有一个“亲戚”。它们是:

  • Number;
  • String;
  • Boolean;
  • Symbol。

因此,我们必须认识到 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。

Number、String和Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。

JavaScript 语言设计上试图模糊对象和基本类型之间的关系,我们日常代码可以把对象的方法在基本类型上使用,比如:

1
console.log("abc".charAt(0)); //a

甚至我们在原型上添加方法,都可以应用于基本类型,比如以下代码,在 Symbol 原型上添加了hello方法,在任何 Symbol 类型变量都可以调用。

1
2
3
4
5
Symbol.prototype.hello = () => console.log("hello");

var a = Symbol("a");
console.log(typeof a); //symbol,a并非对象
a.hello(); //hello,有效

所以最后的一道问题,答案就是. 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。

总结

事实上,“类型”在 JavaScript 中是一个有争议的概念。一方面,标准中规定了运行时数据类型; 另一方面,JS语言中提供了 typeof 这样的运算,用来返回操作数的类型,但 typeof 的运算结果,与运行时类型的规定有很多不一致的地方。我们可以看下表来对照一下。

在表格中,多数项是对应的,但是请注意object——Null和function——Object是特例,我们理解类型的时候需要特别注意这个区别。

从一般语言使用者的角度来看,毫无疑问,我们应该按照 typeof 的结果去理解语言的类型系统。但JS之父本人也在多个场合表示过,typeof 的设计是有缺陷的,只是现在已经错过了修正它的时机。